D:\a\scloud-dns\scloud-dns\src\dns\zones\zone_parser.rs
Line | Count | Source |
1 | | use crate::dns::q_class::DNSClass; |
2 | | use crate::dns::q_type::DNSRecordType; |
3 | | use crate::dns::records::DNSRecord; |
4 | | use crate::dns::zones::Zone; |
5 | | use crate::exceptions::SCloudException; |
6 | | use std::collections::HashMap; |
7 | | use std::fs::File; |
8 | | use std::io::{self, BufRead}; |
9 | | |
10 | | /// Parse a DNS zone file and build an in-memory `Zone` structure. |
11 | | /// |
12 | | /// The zone file must be located in the `zones/` directory and named |
13 | | /// `<qname>.zone`. |
14 | | /// |
15 | | /// This parser supports: |
16 | | /// - `$TTL` directive (default TTL) |
17 | | /// - `$ORIGIN` directive |
18 | | /// - SOA record (unique per zone) |
19 | | /// - Common DNS record types: A, AAAA, NS, MX, TXT, SOA, CNAME, PTR, SRV, CAA, NAPTR |
20 | | /// |
21 | | /// Records are stored by owner name in a `HashMap<String, Vec<DNSRecord>>`. |
22 | | /// The SOA record is stored separately in `zone.soa`. |
23 | | /// |
24 | | /// # Arguments |
25 | | /// * `qname` - The zone name (used to locate the zone file) |
26 | | /// |
27 | | /// # Errors |
28 | | /// Returns `SCloudException` if: |
29 | | /// - the zone file cannot be found |
30 | | /// - the file is empty or unreadable |
31 | | /// - TTL parsing fails |
32 | | /// |
33 | | /// # Example |
34 | | /// ``` |
35 | | /// use crate::dns::zones::zone_parser; |
36 | | /// |
37 | | /// let zone = zone_parser("example.com").expect("Failed to parse zone"); |
38 | | /// |
39 | | /// assert!(zone.soa.is_some()); |
40 | | /// assert!(!zone.records.is_empty()); |
41 | | /// ``` |
42 | | #[allow(unused)] |
43 | 1 | pub fn zone_parser(qname: &str) -> Result<Zone, SCloudException> { |
44 | 1 | let filename = format!("zones/{}.zone", qname); |
45 | 1 | let file = |
46 | 1 | File::open(&filename).map_err(|_| SCloudException::SCLOUD_ZONE_PARSER_FILE_NOT_FOUND)?0 ; |
47 | | |
48 | 1 | let mut zone = Zone { |
49 | 1 | origin: None, |
50 | 1 | name: String::new(), |
51 | 1 | ttl: 3600, |
52 | 1 | soa: None, |
53 | 1 | records: HashMap::new(), |
54 | 1 | }; |
55 | | |
56 | 1 | let mut default_ttl = 3600u32; |
57 | | |
58 | 90 | for line in io::BufReader::new1 (file1 ).lines1 () { |
59 | 90 | let line = line.map_err(|_| SCloudException::SCLOUD_ZONE_PARSER_FILE_EMPTY)?0 ; |
60 | 90 | let line = line.trim(); |
61 | | |
62 | 90 | if line.is_empty() || line73 .starts_with73 (';') { |
63 | 51 | continue; |
64 | 39 | } |
65 | | |
66 | 39 | let line = if let Some(idx6 ) = line.find(';') { |
67 | 6 | &line[..idx] |
68 | | } else { |
69 | 33 | line |
70 | | } |
71 | 39 | .trim(); |
72 | | |
73 | 39 | if line.starts_with("$TTL") { |
74 | 1 | if let Some(ttl_str) = line.split_whitespace().nth(1) { |
75 | 1 | default_ttl = ttl_str |
76 | 1 | .parse::<u32>() |
77 | 1 | .map_err(|_| SCloudException::SCLOUD_ZONE_PARSER_FAILED_TO_READ_TTL_FIELD)?0 ; |
78 | 1 | zone.ttl = default_ttl; |
79 | 0 | } |
80 | 1 | continue; |
81 | 38 | } |
82 | | |
83 | 38 | if line.starts_with("$ORIGIN") { |
84 | 1 | if let Some(origin_str) = line.split_whitespace().nth(1) { |
85 | 1 | zone.origin = Some(origin_str.to_string()); |
86 | 1 | }0 |
87 | 1 | continue; |
88 | 37 | } |
89 | | |
90 | 37 | let mut parts = line.split_whitespace(); |
91 | 37 | let name = match parts.next() { |
92 | 37 | Some(n) => n.to_string(), |
93 | 0 | None => continue, |
94 | | }; |
95 | | |
96 | 37 | let next31 = match parts.next() { |
97 | 31 | Some(n) => n, |
98 | 6 | None => continue, |
99 | | }; |
100 | | |
101 | 31 | let (ttl, class, type_str) = if let Ok(parsed_ttl1 ) = next.parse::<u32>() { |
102 | 1 | let class = parts.next().unwrap_or("IN"); |
103 | 1 | let type_str = parts.next().unwrap_or_default(); |
104 | 1 | (parsed_ttl, class.to_string(), type_str) |
105 | 30 | } else if next.eq_ignore_ascii_case("IN") || next0 .eq_ignore_ascii_case0 ("CH"0 ) { |
106 | 30 | let class = next; |
107 | 30 | let type_str = parts.next().unwrap_or_default(); |
108 | 30 | (default_ttl, class.to_string(), type_str) |
109 | | } else { |
110 | 0 | (default_ttl, "IN".to_string(), next) |
111 | | }; |
112 | | |
113 | 31 | let rclass = match class.to_uppercase().as_str() { |
114 | 31 | "IN" => DNSClass::IN, |
115 | 0 | "CS" => DNSClass::CS, |
116 | 0 | "CH" => DNSClass::CH, |
117 | 0 | "HS" => DNSClass::HS, |
118 | 0 | "NONE" => DNSClass::NONE, |
119 | 0 | "ANY" => DNSClass::ANY, |
120 | 0 | _ => continue, |
121 | | }; |
122 | | |
123 | 31 | let rtype = match type_str.to_uppercase().as_str() { |
124 | 31 | "A" => DNSRecordType::A10 , |
125 | 21 | "AAAA" => DNSRecordType::AAAA4 , |
126 | 17 | "NS" => DNSRecordType::NS3 , |
127 | 14 | "MX" => DNSRecordType::MX2 , |
128 | 12 | "TXT" => DNSRecordType::TXT3 , |
129 | 9 | "SOA" => DNSRecordType::SOA1 , |
130 | 8 | "CNAME" => DNSRecordType::CNAME2 , |
131 | 6 | "PTR" => DNSRecordType::PTR1 , |
132 | 5 | "SRV" => DNSRecordType::SRV2 , |
133 | 3 | "CAA" => DNSRecordType::CAA2 , |
134 | 1 | "NAPTR" => DNSRecordType::NAPTR, |
135 | 0 | _ => continue, |
136 | | }; |
137 | | |
138 | 31 | let value_parts: Vec<&str> = parts.collect(); |
139 | | |
140 | 31 | let value_str = if rtype == DNSRecordType::TXT { |
141 | 3 | value_parts.join(" ") |
142 | | } else { |
143 | 28 | value_parts.join(" ") |
144 | | }; |
145 | | |
146 | 31 | let mut record = DNSRecord { |
147 | 31 | name: name.clone(), |
148 | 31 | rtype: rtype.clone(), |
149 | 31 | rclass: rclass.clone(), |
150 | 31 | ttl, |
151 | 31 | value: value_str, |
152 | 31 | priority: None, |
153 | 31 | weight: None, |
154 | 31 | port: None, |
155 | 31 | flags: None, |
156 | 31 | tag: None, |
157 | 31 | regex: None, |
158 | 31 | replacement: None, |
159 | 31 | order: None, |
160 | 31 | preference: None, |
161 | 31 | }; |
162 | | |
163 | 31 | match rtype { |
164 | | DNSRecordType::MX => { |
165 | 2 | if value_parts.len() >= 2 { |
166 | 2 | if let Ok(prio) = value_parts[0].parse::<u16>() { |
167 | 2 | record.priority = Some(prio); |
168 | 2 | record.value = value_parts[1..].join(" "); |
169 | 2 | }0 |
170 | 0 | } |
171 | | } |
172 | | DNSRecordType::SRV => { |
173 | 2 | if value_parts.len() >= 4 { |
174 | 2 | record.priority = value_parts[0].parse().ok(); |
175 | 2 | record.weight = value_parts[1].parse().ok(); |
176 | 2 | record.port = value_parts[2].parse().ok(); |
177 | 2 | record.value = value_parts[3..].join(" "); |
178 | 2 | }0 |
179 | | } |
180 | | DNSRecordType::CAA => { |
181 | 2 | if value_parts.len() >= 3 { |
182 | 2 | record.flags = value_parts[0].parse().ok(); |
183 | 2 | record.tag = Some(value_parts[1].to_string()); |
184 | 2 | record.value = value_parts[2..].join(" "); |
185 | 2 | }0 |
186 | | } |
187 | | DNSRecordType::NAPTR => { |
188 | 1 | if value_parts.len() >= 5 { |
189 | 1 | record.order = value_parts[0].parse().ok(); |
190 | 1 | record.preference = value_parts[1].parse().ok(); |
191 | 1 | record.flags = Some(value_parts[2].chars().next().unwrap_or_default() as u8); |
192 | 1 | record.regex = Some(value_parts[3].to_string()); |
193 | 1 | record.replacement = Some(value_parts[4].to_string()); |
194 | 1 | }0 |
195 | | } |
196 | 24 | _ => {} |
197 | | } |
198 | | |
199 | 31 | match rtype { |
200 | 1 | DNSRecordType::SOA => zone.soa = Some(record), |
201 | 30 | _ => zone.records.entry(name).or_default().push(record), |
202 | | } |
203 | | } |
204 | | |
205 | 1 | Ok(zone) |
206 | 1 | } |